/******************************************************************************
	Cobalt - utility libraries for building XNA games
	Copyright (C) 2008 Philip Djonov

	This program is free software; you can redistribute it and/or modify it
	under the terms of the GNU General Public License as published by the Free
	Software Foundation; either version 2 of the License, or (at your option)
	any later version.

	This program is distributed in the hope that it will be useful, but
	WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
	or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
	for more details.

	You should have received a copy of the GNU General Public License along
	with this program; if not, write to the
	
		Free Software Foundation, Inc.
		51 Franklin Street, Fifth Floor
		Boston, MA  02110-1301, USA.
******************************************************************************/

using System;
using System.Collections.Generic;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace Cobalt.Framework.Graphics
{
	public sealed class LineLayout
	{
		internal LineLayout()
		{
		}

		internal long textOffset;
		/// <summary>
		/// The index of the first character in this line.
		/// </summary>
		public long TextOffset
		{
			get { return textOffset; }
		}

		internal long charCount;
		/// <summary>
		/// The number of characters processed in this line.
		/// </summary>		
		public long CharacterCount
		{
			get { return charCount; }
		}

		internal int printCharCount;
		/// <summary>
		/// The number of printable characters in this line (includes spaces)
		/// </summary>
		public int PrintedCharacterCount
		{
			get { return printCharCount; }
		}

		internal Vector2 origin;
		public Vector2 Origin
		{
			get { return origin; }
		}

		internal Vector2 size;
		public Vector2 Size
		{
			get { return size; }
		}

		internal float justifySpacing;
		public float JustifySpacing
		{
			get { return justifySpacing; }
		}

	}

	public enum LineFlags
	{
		None			= 0x0000,
		/* SmallCaps	= 0x0001, << ToDo */
		SnapLineOrigins	= 0x0002,
	}

	public enum LayoutFlags
	{
		None			= 0x0000,

		Near			= 0x0000, //default
		Center			= 0x0001,
		Far				= 0x0002,
		Justified		= 0x0003,

		/// <summary>
		/// Turns off word wrapping, forcing strings to clip to the nearest character.
		/// This flag is only valid for the <c>xLayout</c> parameter.
		/// </summary>
		NoWrap			= 0x0100,
		/// <summary>
		/// Turns off clipping against the layout rectangle.
		/// </summary>
		NoClip			= 0x0200,
	}

	public sealed class LineLayoutEngine
	{
		private const int LineLayoutTypeMask = 0x000F;

		private int lineCount;
		private LineLayout[] lines;

		private LineLayout tmpLine = new LineLayout();

		public LineLayoutEngine( int maxLines )
		{
			if( maxLines <= 0 )
				throw new ArgumentOutOfRangeException();

			lines = new LineLayout[maxLines];
			for( int i = 0; i < lines.Length; i++ )
				lines[i] = new LineLayout();
		}

		public int Capacity { get { return lines.Length; } }

		public int Count
		{
			get { return lineCount; }
		}

		public LineLayout GetLine( int index )
		{
			if( index < 0 || index > lineCount )
				throw new ArgumentOutOfRangeException();

			return lines[index];
		}

		internal Font font;
		internal float textScale;

		internal int sourceOffset;
		internal char[] sourceChars;
		internal string sourceString;

		private ITextHelper textHelper;
		public ITextHelper TextHelper
		{
			get { return textHelper; }
			set { textHelper = value; }
		}

		public void Reset()
		{
			font = null;
			textScale = 0;

			lineCount = 0;

			sourceOffset = 0;
			sourceChars = null;
			sourceString = null;
		}

		private const int NoLineLimit = 0x7FFFFFFF;
		private int maxLines = NoLineLimit;
		public int? MaxLines
		{
			get { return maxLines != NoLineLimit ? (int?)maxLines : (int?)null; }
			set
			{
				if( value == null )
				{
					maxLines = NoLineLimit;
					return;
				}

				int val = value.Value;

				if( val < 0 )
					throw new ArgumentOutOfRangeException( "MaxLines" );

				maxLines = val;
			}
		}

		private const int NoCharLimit = -1;
		private int maxCharsPerLine = NoCharLimit;
		public int? MaxCharsPerLine
		{
			get { return maxCharsPerLine != NoCharLimit ? (int?)maxCharsPerLine : (int?)null; }
			set
			{
				if( value == null )
				{
					maxCharsPerLine = NoCharLimit;
					return;
				}

				int val = value.Value;

				if( val < 0 )
					throw new ArgumentOutOfRangeException( "MaxCharsPerLine" );

				maxCharsPerLine = val;
			}
		}

		private LayoutFlags xLayout = LayoutFlags.Near;
		public LayoutFlags HorizontalLayout
		{
			get { return xLayout; }
			set { xLayout = value; }
		}

		private LayoutFlags yLayout = LayoutFlags.Near;
		public LayoutFlags VerticalLayout
		{
			get { return yLayout; }
			set { yLayout = value; }
		}

		private LineFlags flags = LineFlags.SnapLineOrigins;
		public LineFlags LineFlags
		{
			get { return flags; }
			set { flags = value; }
		}

		/// <summary>
		/// Performs line layout.
		/// </summary>
		/// <param name="text">The string to layout.</param>
		/// <param name="count">The number of characters to process, or <c>NoCharLimit</c> to go till the end of the string.</param>
		/// <param name="font">The font the string will be laid out with.</param>
		/// <param name="textScale">The text scale parameter.</param>
		/// <param name="origin">The top-left corner of the layout rectangle.</param>
		/// <param name="size">The size of the layout rectangle.</param>
		/// <returns>
		/// The number of lines laid out.
		/// </returns>
		private unsafe int LayoutLines( char* text, int count,
			Font font, float textScale, Vector2 origin, Vector2 size, bool onlyCounting )
		{
			if( font == null )
				throw new ArgumentNullException( "font" );

			if( textHelper == null )
				throw new InvalidOperationException( "TextHelper has not been set." );

			this.font = font;
			this.textScale = textScale;
			this.lineCount = 0;

			bool xClip = (xLayout & LayoutFlags.NoClip) == LayoutFlags.None;
			bool yClip = (yLayout & LayoutFlags.NoClip) == LayoutFlags.None;
			bool wordWrap = (xLayout & LayoutFlags.NoWrap) == LayoutFlags.None;

			int maxLines = this.maxLines;
			if( onlyCounting )
			{
				lines[0].charCount = 0;
				lines[0].printCharCount = 0;
			}
			else if( maxLines > lines.Length )
				maxLines = lines.Length;

			Font.FontPoint basePoint = font.GetPoint( textHelper.GetPointSize( textScale ) );
			Vector2 baseScale = textHelper.GetGlyphScale( textScale, basePoint.PointSize );

			char* s = text;

			int lineCount;
			for( lineCount = 0; lineCount < maxLines; lineCount++ )
			{
				LineLayout line = onlyCounting ? tmpLine : lines[lineCount];

				int spaceCount = 0;
				float widthAtLastSpace = 0;
				char* lastSpace = s;

				float prevKernC = 0;

				line.textOffset = s - text;
				line.charCount = 0;
				line.printCharCount = 0;

				line.origin = new Vector2( 0, basePoint.LineHeight * baseScale.Y * lineCount );
				line.size = new Vector2( 0, basePoint.LineHeight * baseScale.Y );

				line.justifySpacing = 0;

				for( ; count > 0; )
				{
					int len = textHelper.ProcessFormattingSequence( s, count, null );
					if( len > 0 )
					{
						s += len;
						count -= len;

						continue;
					}

					int ch = Graphics.TextHelper.DecodeCharacter( s, out len );
					s += len;
					count -= len;

					if( ch == '\n' )
					{
						//end of line marker
						s -= len; //back up so outer loop notices the reason
						break;
					}

					int gIdx = font.GetCharacterIndex( ch );

					if( gIdx < 0 )
						//missing glyph, skip it
						continue;

					if( ch == ' ' )
					{
						//record white space
						lastSpace = s - 1;
						widthAtLastSpace = line.size.X;
						spaceCount++;
					}

					float w = basePoint.Glyphs[gIdx].KernA * baseScale.X;
					if( line.printCharCount > 0 )
						w += prevKernC;
					else if( w < 0 )
						w = 0;

					w += basePoint.Glyphs[gIdx].KernB * baseScale.X;
					prevKernC = basePoint.Glyphs[gIdx].KernC * baseScale.X;

					if( xClip && line.size.X + w > size.X ||
						maxCharsPerLine != NoCharLimit && line.printCharCount + 1 >= maxCharsPerLine )
					{
						//line is too long, clip it

						if( wordWrap && spaceCount > 0 )
						{
							s = lastSpace;
							line.size.X = widthAtLastSpace;
							spaceCount--;

							line.justifySpacing = (float)spaceCount; //do the division later
						}

						break;
					}

					if( basePoint.Glyphs[gIdx].Height * baseScale.Y > line.size.Y )
						line.size.Y = basePoint.Glyphs[gIdx].Height * baseScale.Y;

					line.size.X += w;

					line.printCharCount++;
				}

				if( yClip && line.origin.Y + line.size.Y > size.Y )
					//abort, do not count this line!
					break;

				if( !onlyCounting )
				{
					switch( xLayout & (LayoutFlags)LineLayoutTypeMask )
					{
						case LayoutFlags.Near:
							line.justifySpacing = 0;
							break;

						case LayoutFlags.Center:
							line.origin.X = (size.X - line.size.X) / 2;
							line.justifySpacing = 0;
							break;

						case LayoutFlags.Far:
							line.origin.X = size.X - line.size.X;
							line.justifySpacing = 0;
							break;

						case LayoutFlags.Justified:
							if( line.justifySpacing > 0 )
								line.justifySpacing = (size.X - line.size.X) / line.justifySpacing;
							break;

						default:
							goto case LayoutFlags.Near;
					}
				}

				if( count > 0 )
				{
					switch( s[0] )
					{
						case '\n':
						case ' ':
							s++;
							break;
					}
				}

				line.charCount = s - (text + line.textOffset);

				if( onlyCounting )
				{
					lines[0].charCount += line.charCount;
					lines[0].printCharCount += line.printCharCount;
				}

				if( count <= 0 )
				{
					lineCount++;
					break;
				}
			}

			if( !onlyCounting )
			{
				this.lineCount = lineCount;

				if( lineCount > 0 )
				{
					LineLayout lastLine = lines[lineCount - 1];

					float yScale = 1, yBias = origin.Y - basePoint.LineHeight * baseScale.Y * 0.05F;
					float linesHeight = lastLine.origin.Y + lastLine.size.Y;

					if( linesHeight > size.Y && lineCount > 1 )
					{
						const float ShrinkScale = 1F;

						yScale *= ShrinkScale;
						yBias += linesHeight * (1 - ShrinkScale) * 0.5F;

						linesHeight = lastLine.origin.Y * ShrinkScale + lastLine.size.Y;
					}

					switch( yLayout & (LayoutFlags)LineLayoutTypeMask )
					{
						case LayoutFlags.Near:
							break;

						case LayoutFlags.Center:
							yBias += (size.Y - linesHeight / 2);
							break;

						case LayoutFlags.Far:
							yBias += size.Y - linesHeight;
							break;

						case LayoutFlags.Justified:
							if( lastLine.origin.Y > 0 )
							{
								yScale *= (size.Y - lastLine.size.Y) / lastLine.origin.Y;
								break;
							}
							goto case LayoutFlags.Center;

						default:
							goto case LayoutFlags.Near;
					}

					for( int i = 0; i < lineCount; i++ )
					{
						lines[i].origin.X += origin.X;
						lines[i].origin.Y = lines[i].origin.Y * yScale + yBias;
					}

					if( (flags & LineFlags.SnapLineOrigins) != LineFlags.None )
					{
						//snapping the y values to pixel boundaries helps
						//*a lot* with fighting texture filtering distortion

						float delta = 0;

						for( int i = 0; i < lineCount; i++ )
						{
							float y = lines[i].origin.Y + delta;
							delta = y - (int)y;
							lines[i].origin.Y = (int)y;

							lines[i].origin.X = (int)lines[i].origin.X;
						}
					}
				}
			}

			return lineCount;
		}

		public unsafe void LayoutLines( string text, int index, int count,
			Font font, float textScale, Vector2 origin, Vector2 size )
		{
			if( text == null )
				throw new ArgumentNullException( "text" );
			if( index < 0 || count < 0 || index + count > text.Length )
				throw new ArgumentOutOfRangeException();

			sourceString = text;
			sourceOffset = index;
			sourceChars = null;

			fixed( char* s = text )
				LayoutLines( s + index, count, font, textScale, origin, size, false );
		}

		public void LayoutLines( string text, Font font, float textScale, Vector2 origin, Vector2 size )
		{
			if( text == null )
				throw new ArgumentNullException( "text" );

			LayoutLines( text, 0, text.Length, font, textScale, origin, size );
		}

		public void LayoutLines( string text, Font font, float textScale, Vector2 origin )
		{
			LayoutLines( text, 0, text.Length, font, textScale, origin, Vector2.Zero );
		}

		public unsafe void LayoutLines( char[] text, int index, int count,
			Font font, float textScale, Vector2 origin, Vector2 size )
		{
			if( text == null )
				throw new ArgumentNullException( "text" );
			if( index < 0 || count < 0 || index + count > text.Length )
				throw new ArgumentOutOfRangeException();

			sourceChars = text;
			sourceOffset = index;
			sourceString = null;

			fixed( char* s = text )
				LayoutLines( s + index, count, font, textScale, origin, size, false );
		}

		public void LayoutLines( char[] text, Font font, float textScale, Vector2 origin, Vector2 size )
		{
			if( text == null )
				throw new ArgumentNullException( "text" );

			LayoutLines( text, 0, text.Length, font, textScale, origin, size );
		}

		public void LayoutLines( char[] text, Font font, float textScale, Vector2 origin )
		{
			LayoutLines( text, 0, text.Length, font, textScale, origin, Vector2.Zero );
		}

		public void GetLayoutBounds( out Vector2 origin, out Vector2 size )
		{
			if( lineCount == 0 )
			{
				origin = Vector2.Zero;
				size = Vector2.Zero;
				return;
			}

			origin = lines[0].origin;

			for( int i = 1; i < lineCount; i++ )
			{
				LineLayout l = lines[i];

				if( l.origin.X < origin.X )
					origin.X = l.origin.X;

				if( l.origin.Y < origin.Y )
					origin.Y = l.origin.Y;
			}

			size = lines[0].size;
			for( int i = 0; i < lineCount; i++ )
			{
				LineLayout l = lines[i];

				if( l.origin.X + l.size.X > origin.X + size.X )
					size.X = l.origin.X + l.size.X - origin.X;

				if( l.origin.Y + l.size.Y > origin.Y + size.Y )
					size.Y = l.origin.Y + l.size.Y - origin.Y;
			}
		}
	}

}